Skip to content

feat: add AgentAdapter abstraction with Codex CLI support#95

Open
ayeo wants to merge 6 commits into
mainfrom
feat/agent-adapter-codex
Open

feat: add AgentAdapter abstraction with Codex CLI support#95
ayeo wants to merge 6 commits into
mainfrom
feat/agent-adapter-codex

Conversation

@ayeo
Copy link
Copy Markdown

@ayeo ayeo commented Apr 3, 2026

Description

Replace hardcoded Claude Code transcript parsing with an extensible AgentAdapter trait and registry. Each AI coding agent gets its own adapter for event mapping, file change extraction, and transcript record parsing. Adds full Codex CLI support.

What's included

  • AgentAdapter trait with ClaudeCode, Codex, and Default adapters in tracevault-core
  • Codex transcript parsing — handles response_item/message, custom_tool_call, event_msg, apply_patch file changes from transcript chunks
  • CLI changes — protocol v2, --agent flag for stream command, Codex-compatible hook response format
  • tracevault init --agent codex — installs Codex hooks in .codex/hooks.json
  • AgentBadge component — shows agent type with icon on session list and detail views
  • Cleanup — removed old hardcoded extract_file_change/is_file_modifying_tool from streaming.rs
  • 34 adapter tests, UTF-8 safe truncation, code review fixes

How it works

The server resolves the adapter from sessions.tool column (set by CLI via --agent flag). During ingestion (stream.rs), the adapter extracts tokens and file changes. During display (session_detail.rs, traces_ui.rs), it parses transcript chunks into TranscriptRecords for the frontend.

Codex file modifications come exclusively through transcript chunks (custom_tool_call with apply_patch), not through hook ToolUse events — the adapter handles this via extract_file_changes_from_transcript.

Checklist

  • Tests added or updated
  • cargo fmt passes
  • cargo clippy passes

🤖 Generated with Claude Code

@ayeo ayeo force-pushed the feat/agent-adapter-codex branch 5 times, most recently from d6a4cba to 555e6c2 Compare April 29, 2026 07:41
ayeo pushed a commit that referenced this pull request Apr 30, 2026
…hooks and CLI flag

Claude Code was rewritten in #95 instead of being ported from
session_detail.rs::parse_record on main, which introduced display
regressions: Bash/Glob toolUseResult lost their tool_name (wrong
nested-key lookup), tool_result blocks lost their text body (read
`text` instead of `content`), and assistant text formatting lost the
\n\n separator and `[thinking] ` prefix. The parser is now a faithful
port — same fields, same fallbacks, same format strings.

Token extraction now mirrors main: presence of `usage` gates the whole
RecordUsage and individual missing fields default to 0, instead of
aborting on missing input/output_tokens.

Codex adapter:
- SessionStart matcher widened from "startup|resume" to "" so the hook
  also fires on /clear (verified against openai/codex sources).
- The user-message system-prompt filter no longer drops every message
  starting with `<`. It now matches only the seven known Codex
  injection tags from codex protocol.rs (user_instructions,
  environment_context, apps_instructions, skills_instructions,
  plugins_instructions, collaboration_mode, realtime_conversation),
  preserving legitimate <div>/<svg>/<T>-style user questions.
- File changes extracted from transcript chunks now use the chunk's
  own RFC 3339 timestamp (with fallback to the hook delivery time)
  rather than stamping every batched patch with the hook arrival time.

CLI:
- `tracevault init --agent <name>` is now additive: Claude Code hooks
  are always installed, additional --agent values are appended and
  deduplicated (with `claude` aliased to `claude-code`). Previously
  --agent codex replaced rather than augmented the default, so users
  following the README ended up without Claude hooks.
- The success print now reflects which agents were actually installed
  instead of unconditionally claiming "Claude Code hooks installed".
- README CLI table reworded to match the additive behavior.

Cleanup: deduplicated adapter.is_file_modifying call in
service/stream.rs (the result is already in `store_response`).

Tests: 16 new adapter tests cover the regressed Claude Code parser
paths (Bash/Glob/tool_result/thinking/system unknown subtype/progress
edge cases) plus Codex token_usage edge cases and the Codex
system-prompt whitelist. 5 new init tests cover the additive --agent
behavior, dedup of `claude`/`claude-code` aliases, and the Codex
SessionStart match-all matcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@hashedone hashedone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review notes

Thanks for this — the adapter abstraction is clean and the capability-flag pattern is the right approach. A few things worth addressing before merge, ranging from a potential silent data loss to minor maintenance notes.


🟡 Medium: chunk_index collision between transcript chunks and synthetic tool events

In service/stream.rs, when provides_transcript_file_changes() is true (Codex), synthetic tool events are inserted with event_index: chunk_index:

event_index: chunk_index,  // reusing transcript chunk index

The events table has UNIQUE(session_id, event_index) with ON CONFLICT DO NOTHING. If a Codex session also receives hook ToolUse events whose event_index values overlap with transcript chunk indices, the second insert is silently dropped. There is no namespace separation between the two index spaces. This does not affect CC (gated by provides_transcript_file_changes = false), but for Codex sessions it could silently lose events without any error.

Suggestion: either offset synthetic event indices (e.g. use a separate counter namespace), or assert they come from a different range.


🟡 Medium: AgentAdapterRegistry constructed per hook call in CLI

In crates/tracevault-cli/src/commands/stream.rs:

let registry = AgentAdapterRegistry::new();
let adapter = registry.get(agent);

This constructs a new registry (allocating all adapters) on every hook invocation — PostToolUse fires on every tool call. The allocation is cheap but unnecessary. Consider constructing just the needed adapter directly, or passing the agent name through to a shared/static registry.


🔵 Low: Codex hooks.json wraps entries under a "hooks" key — needs spec verification

In CodexAdapter::install_hooks:

config_obj.insert("hooks".to_string(), hooks_json());

This writes {"hooks": {"PostToolUse": [...]}} to .codex/hooks.json. But based on the Codex docs, hooks are defined at the top level of hooks.json — the file contents should be {"PostToolUse": [...]} directly, not wrapped under a "hooks" key. Worth verifying against the current Codex hook spec before shipping, otherwise the hooks may silently not fire.


🔵 Low: store_response variable reused as file-change extraction gate

let store_response = adapter.is_file_modifying(tool_name);
// ...
if store_response {
    // file change extraction

store_response was semantically named for "should we persist the tool_response blob" but is reused as the gate for file change extraction. For CC these happen to be the same tool set (Write/Edit/Bash), but the coupling is implicit. A future adapter that has a file-modifying tool with large/binary output would need to override is_file_modifying to false to avoid storing the blob — which would also skip file change extraction. Worth separating into two distinct capability queries.


🔵 Low: CC protocol v1 fallback fragility

let agent_name = tool.as_deref().unwrap_or("claude-code");

This works correctly for v1 (where tool is absent). Worth a comment noting that if a future CC version sends v2 without tool: "claude-code", it would fall through to DefaultAdapter and lose all token/file extraction silently. The CLI sets tool explicitly so this is safe today, but the silent fallback is worth documenting.


🔵 Low: Hardcoded Codex system prompt XML tags — maintenance drift risk

const CODEX_SYSTEM_PROMPT_TAGS: &[&str] = &[
    "<user_instructions>",
    "<environment_context>",
    ...

These are sourced from openai/codex protocol.rs. If Codex adds or renames tags in a future release, this filter will either pass system prompts through (display noise) or incorrectly filter legitimate user messages. A comment noting the Codex source file and version where these were verified would help future maintainers know when to re-check.

Michał Grabowski and others added 6 commits May 29, 2026 15:30
Replace hardcoded Claude Code transcript parsing with an extensible
AgentAdapter trait. Each agent gets its own adapter for event mapping,
file change extraction, transcript parsing, and token/model extraction.

- AgentAdapter trait with ClaudeCode, Codex, and Default adapters
- Codex transcript parsing: response_item, custom_tool_call, event_msg,
  apply_patch file changes from transcript chunks
- CLI: protocol v2, repeatable --agent flag for init/stream
- tracevault init --agent codex installs .codex/hooks.json
- AgentBadge component with per-agent icon on session list/detail
- Server uses AgentAdapterRegistry on AppState
- Removes old hardcoded extract_file_change/is_file_modifying_tool

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hooks and CLI flag

Claude Code was rewritten in #95 instead of being ported from
session_detail.rs::parse_record on main, which introduced display
regressions: Bash/Glob toolUseResult lost their tool_name (wrong
nested-key lookup), tool_result blocks lost their text body (read
`text` instead of `content`), and assistant text formatting lost the
\n\n separator and `[thinking] ` prefix. The parser is now a faithful
port — same fields, same fallbacks, same format strings.

Token extraction now mirrors main: presence of `usage` gates the whole
RecordUsage and individual missing fields default to 0, instead of
aborting on missing input/output_tokens.

Codex adapter:
- SessionStart matcher widened from "startup|resume" to "" so the hook
  also fires on /clear (verified against openai/codex sources).
- The user-message system-prompt filter no longer drops every message
  starting with `<`. It now matches only the seven known Codex
  injection tags from codex protocol.rs (user_instructions,
  environment_context, apps_instructions, skills_instructions,
  plugins_instructions, collaboration_mode, realtime_conversation),
  preserving legitimate <div>/<svg>/<T>-style user questions.
- File changes extracted from transcript chunks now use the chunk's
  own RFC 3339 timestamp (with fallback to the hook delivery time)
  rather than stamping every batched patch with the hook arrival time.

CLI:
- `tracevault init --agent <name>` is now additive: Claude Code hooks
  are always installed, additional --agent values are appended and
  deduplicated (with `claude` aliased to `claude-code`). Previously
  --agent codex replaced rather than augmented the default, so users
  following the README ended up without Claude hooks.
- The success print now reflects which agents were actually installed
  instead of unconditionally claiming "Claude Code hooks installed".
- README CLI table reworded to match the additive behavior.

Cleanup: deduplicated adapter.is_file_modifying call in
service/stream.rs (the result is already in `store_response`).

Tests: 16 new adapter tests cover the regressed Claude Code parser
paths (Bash/Glob/tool_result/thinking/system unknown subtype/progress
edge cases) plus Codex token_usage edge cases and the Codex
system-prompt whitelist. 5 new init tests cover the additive --agent
behavior, dedup of `claude`/`claude-code` aliases, and the Codex
SessionStart match-all matcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hic API

Pull adapter-specific knowledge out of `service/stream.rs`. Previously
the stream service hardcoded Codex chunk-shape lookups (`payload.name`,
`payload` cloning, RFC 3339 timestamp parsing) and used two extraction
methods with different return types (`Vec<ExtractedFileChange>` vs
`Vec<TranscriptFileChange>`), so the call site had to reach into chunk
internals to fill in tool_name / tool_input / timestamp.

The trait now exposes two symmetric methods returning the same
`FileChangeRecord` type:

  fn file_changes_from_hook(&self, tool, input, ts) -> Vec<FileChangeRecord>
  fn file_changes_from_transcript(&self, chunk, fallback_ts)
      -> Vec<FileChangeRecord>

Each adapter overrides at most one. Defaults return empty. The
`FileChangeRecord` carries everything the persistence layer needs
(change, tool_name, tool_input, timestamp), so `stream.rs` just
iterates and inserts — no chunk shape knowledge anywhere outside the
adapter that owns that format.

Claude path is preserved bit-for-bit against main:

* `is_file_modifying` gate around the hook-extract loop is kept, so
  Read/Glob/etc. skip the call entirely (matches main's
  `if is_file_modifying_tool { ... }`).
* New `provides_transcript_file_changes()` capability flag (default
  false) gates the per-line transcript-extract loop. Claude returns
  false → the `file_changes_from_transcript` method is never invoked
  for Claude transcript lines, exactly as on main where no equivalent
  call existed.
* `file_changes_from_hook` for Claude wraps the same Write/Edit logic
  that lived in `extract_file_change` on main; the resulting DB writes
  have identical fields and timestamps (record.timestamp = req.timestamp).

CLI: replace the hardcoded `match agent.as_str() { "claude-code" => ...,
"codex" => ... }` in `main.rs` with `adapter.display_name()` and
`adapter.hooks_install_path()` from the trait, so adding a new agent
no longer requires touching the print-message code.

Codex: `file_changes_from_transcript` now resolves the chunk's RFC 3339
timestamp internally and returns it in each record, replacing the
duplicated timestamp logic that previously lived in `stream.rs`.
The `provides_transcript_file_changes` override is `true`.

Tests: 51 adapter tests (was 50), including a new fallback case
verifying that a chunk with no top-level timestamp falls back to the
hook delivery time. All hook/transcript extraction tests updated to
the new method names and return type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing --agent

Move the "claude"/"claude-code" alias resolution and dedup off the CLI and
onto the AgentAdapter::name() canonical id — the registry already maps both
strings to the same adapter, so the manual match was redundant. Dedup now
runs against the adapter's own id, not the user-provided string.

Change semantics: --agent codex installs only Codex hooks. Claude Code is
installed only when --agent is omitted entirely (default), instead of being
appended unconditionally to every --agent invocation. .gitignore entries are
derived from each installed adapter's hooks_install_path(), so a codex-only
init no longer pins .claude/settings.json into the ignore list.
Multi-agent split caused subtle drift on the Claude code path. Restore
parity with pre-multi-agent main:

- wire_protocol_version() trait method (default v2); Claude overrides
  to v1 so request bytes match main
- persists_model_without_usage() capability flag (default false); Codex
  sets it true. Server stream gate becomes
  has_tokens || (flag && model.is_some()), so Claude's update_tokens
  stays token-presence-only as in main
- ClaudeCodeAdapter parser locks onto first tool_use block via
  seen_tool_use flag (matches main's arr.iter().find() semantics)
- CLI stream uses adapter.wire_protocol_version() / adapter.name() for
  protocol_version + tool fields
- init.rs installs hooks after .gitignore update (matches main order)

Also: CLI init prints actually-installed gitignore entries instead of
hardcoded paths, and a comment marks _event_type as unused (routing is
via hook_event_name from stdin).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…E with --agent semantics

Codex Stop hook entry was missing the `matcher` field that all other
lifecycle hooks (SessionStart, PreToolUse, PostToolUse) already carry,
risking a silent no-op if Codex requires the field. README also still
described `--agent` as additive ("in addition to the Claude Code hooks")
even though the flag has been replacement-only since 6fad80f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ayeo ayeo force-pushed the feat/agent-adapter-codex branch from bb61ee2 to d858db4 Compare May 29, 2026 14:09

let settings_path = claude_dir.join("settings.json");
let mut settings: serde_json::Value = if settings_path.exists() {
let content = fs::read_to_string(&settings_path)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal attack possible - medium severity
The application constructs file paths using untrusted data, potentially leading to a path traversal vulnerability. An attacker could manipulate these inputs to access, create, or overwrite sensitive files.

Show fix

Remediation: To mitigate path traversal vulnerabilities, validate and sanitize all user input used in file path construction, and enforce strict access controls to limit file access to only necessary directories and files with methods like File::set_permissions if possible.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info


let hooks_path = codex_dir.join("hooks.json");
let mut config: serde_json::Value = if hooks_path.exists() {
let content = fs::read_to_string(&hooks_path)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal attack possible - medium severity
The application constructs file paths using untrusted data, potentially leading to a path traversal vulnerability. An attacker could manipulate these inputs to access, create, or overwrite sensitive files.

Show fix

Remediation: To mitigate path traversal vulnerabilities, validate and sanitize all user input used in file path construction, and enforce strict access controls to limit file access to only necessary directories and files with methods like File::set_permissions if possible.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

}
}

fn parse_response_item(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_response_item's message path nests multiple filters and iterator transforms; add early guards (e.g., return None for developer role or empty content) to simplify and flatten the extraction logic.

Details

✨ AI Reasoning
​The response_item handler performs nested extraction and filtering of message content inside a match arm, with logic that skips developer messages and filters block types. The meaningful text extraction is buried inside chained iterator operations; simple guard checks (e.g., early return for developer role or empty content array) before heavy processing would reduce nesting and improve readability.

🔧 How do I fix it?
Place parameter validation and guard clauses at the function start. Use early returns to reduce nesting levels and improve readability.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

None
};

let tr = TranscriptRecord {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local variable 'tr' is cryptic; rename to 'transcript_record' or 'record' for clarity.

Details

✨ AI Reasoning
​A local variable named 'tr' was introduced to hold a TranscriptRecord constructed from an adapter-parsed object. The name is a terse abbreviation lacking clear semantic meaning in a loop of substantial size, which reduces readability for future maintainers and makes code navigation harder. A more descriptive name would clarify intent without changing logic.

🔧 How do I fix it?
Use descriptive names for variables (except standard loop counters i, j, k and math variables x, y).

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment on lines +140 to +145
let adapter = registry.get(agent);
let path = adapter.hooks_install_path();
if path.is_empty() {
println!("{} hooks installed", adapter.display_name());
} else {
println!("{} hooks installed ({})", adapter.display_name(), path);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Success output uses registry.get(agent) and may print hooks installed for unknown agents that init_in_directory skipped. This can report installation that never happened.

Show fix
Suggested change
let adapter = registry.get(agent);
let path = adapter.hooks_install_path();
if path.is_empty() {
println!("{} hooks installed", adapter.display_name());
} else {
println!("{} hooks installed ({})", adapter.display_name(), path);
if let Some(adapter) = registry.get(agent) {
let path = adapter.hooks_install_path();
if path.is_empty() {
println!("{} hooks installed", adapter.display_name());
} else {
println!("{} hooks installed ({})", adapter.display_name(), path);
}
Details

✨ AI Reasoning
​​1) The initialization flow attempts to install extra agent hooks only when lookup succeeds; unknown agent names are explicitly skipped with a warning.
​2) The success output flow iterates the same agents input but resolves via a different lookup call that does not preserve the previous skip decision.
​3) This means a user can be told hooks were installed for an agent that was actually skipped.
​4) That is a concrete logic inconsistency in control flow and user-facing outcome, not a style issue.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment on lines +318 to +355
let record_type = if role == "assistant" {
"assistant"
} else {
"user"
};
let text = payload
.get("content")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|block| {
let block_type = block.get("type").and_then(|v| v.as_str())?;
if block_type == "input_text" || block_type == "output_text" {
let t = block.get("text").and_then(|v| v.as_str())?;
// Codex injects system context into the user role wrapped
// in known XML tags (see openai/codex protocol.rs).
// Skip only those — a blunt `starts_with('<')` would also
// drop legitimate user questions about HTML/JSX/XML snippets.
if role == "user" && is_codex_system_prompt(t) {
return None;
}
Some(t.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n\n")
})
.filter(|s| !s.is_empty());
// Skip if no meaningful text
text.as_ref()?;
Some(ParsedTranscriptRecord {
record_type: record_type.to_string(),
timestamp: timestamp.clone(),
content_types: vec!["text".to_string()],
tool_name: None,
text,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fix the Use early returns and guard clauses issue detected on line: 283.

Show Fix

Aikido AutoFix Patch Suggestion - low confidence
This patch mitigates deeply nested control flow in parse_response_item's "message" branch by introducing early guard clauses for empty content arrays and empty extracted text, flattening the extraction logic and reducing nesting.

Suggested change
let record_type = if role == "assistant" {
"assistant"
} else {
"user"
};
let text = payload
.get("content")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|block| {
let block_type = block.get("type").and_then(|v| v.as_str())?;
if block_type == "input_text" || block_type == "output_text" {
let t = block.get("text").and_then(|v| v.as_str())?;
// Codex injects system context into the user role wrapped
// in known XML tags (see openai/codex protocol.rs).
// Skip only those — a blunt `starts_with('<')` would also
// drop legitimate user questions about HTML/JSX/XML snippets.
if role == "user" && is_codex_system_prompt(t) {
return None;
}
Some(t.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n\n")
})
.filter(|s| !s.is_empty());
// Skip if no meaningful text
text.as_ref()?;
Some(ParsedTranscriptRecord {
record_type: record_type.to_string(),
timestamp: timestamp.clone(),
content_types: vec!["text".to_string()],
tool_name: None,
text,
let content_array = payload.get("content")?.as_array()?;
if content_array.is_empty() {
return None;
}
let record_type = if role == "assistant" {
"assistant"
} else {
"user"
};
let text = content_array
.iter()
.filter_map(|block| {
let block_type = block.get("type").and_then(|v| v.as_str())?;
if block_type == "input_text" || block_type == "output_text" {
let t = block.get("text").and_then(|v| v.as_str())?;
// Codex injects system context into the user role wrapped
// in known XML tags (see openai/codex protocol.rs).
// Skip only those — a blunt `starts_with('<')` would also
// drop legitimate user questions about HTML/JSX/XML snippets.
if role == "user" && is_codex_system_prompt(t) {
return None;
}
Some(t.to_string())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n\n");
// Skip if no meaningful text
if text.is_empty() {
return None;
}
Some(ParsedTranscriptRecord {
record_type: record_type.to_string(),
timestamp: timestamp.clone(),
content_types: vec!["text".to_string()],
tool_name: None,
text: Some(text),

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment on lines +245 to +267
fn parse_assistant_record(
&self,
chunk: &serde_json::Value,
record_type: String,
timestamp: Option<String>,
) -> Option<ParsedTranscriptRecord> {
let message = match chunk.get("message") {
Some(m) => m,
None => {
return Some(ParsedTranscriptRecord {
record_type,
timestamp,
content_types: Vec::new(),
tool_name: None,
text: None,
raw_input_tokens: None,
raw_output_tokens: None,
raw_cache_read_tokens: None,
raw_cache_write_tokens: None,
model: None,
});
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_assistant_record has a long nested accumulation phase; add early guards (e.g., early return on empty content/usage) to flatten logic and clarify the primary parsing path.

Show fix
Suggested change
fn parse_assistant_record(
&self,
chunk: &serde_json::Value,
record_type: String,
timestamp: Option<String>,
) -> Option<ParsedTranscriptRecord> {
let message = match chunk.get("message") {
Some(m) => m,
None => {
return Some(ParsedTranscriptRecord {
record_type,
timestamp,
content_types: Vec::new(),
tool_name: None,
text: None,
raw_input_tokens: None,
raw_output_tokens: None,
raw_cache_read_tokens: None,
raw_cache_write_tokens: None,
model: None,
});
}
};
fn parse_assistant_record(
let message = chunk.get("message");
if message.is_none() {
return Some(ParsedTranscriptRecord {
record_type,
timestamp,
content_types: Vec::new(),
tool_name: None,
text: None,
raw_input_tokens: None,
raw_output_tokens: None,
raw_cache_read_tokens: None,
raw_cache_write_tokens: None,
model: None,
});
}
let message = message.unwrap();
Details

✨ AI Reasoning
​The function parses an assistant message into many derived fields using a long procedural block that maintains seen_tool_use, content_types, and text_parts. Much of the main construction happens after iterating and conditional accumulation, making the flow harder to follow. Extracting early guards/continues for trivial non-cases (e.g., empty content array) would reduce nesting and clarify the primary transformation. The function is a good candidate for guard clauses to return simple empty/none cases before the heavier accumulation logic.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants